#set( $symbol_pound = '#' )
#set( $symbol_dollar = '$' )
#set( $symbol_escape = '\' )
/*
 * Copyright © 2014, Finium Sdn Bhd, All Rights Reserved
 * 
 * EncryptionUtils.java
 * Modification History
 * *************************************************************
 * Date         Author                          Comment
 * Jan 3, 2014  Venkaiah Chowdary Koneru        Created
 * *************************************************************
 */
package ${package}.support;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;

import ${package}.AppConstants;

import org.springframework.core.NestedRuntimeException;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.util.Assert;

/**
 * A static utility class that can encrypt and decrypt text.
 * 
 * <p>
 * This class is useful if you have simple needs and wish to use the DESede
 * encryption cipher. More sophisticated requirements will need to use the Java
 * crypto libraries directly.
 * 
 * @author Venkaiah Chowdary Koneru
 */
public final class EncryptionUtils
{
    private static final int MINIMUM_KEY_LENGTH = 24;

    /**
     * This is a static class that should not be instantiated.
     */
    private EncryptionUtils()
    {
    }

    /**
     * Converts a String into a byte array using UTF-8, falling back to the
     * platform's default character set if UTF-8 fails.
     * 
     * @param input
     *            the input (required)
     * @return a byte array representation of the input string
     */
    public static byte[] stringToByteArray(String input)
    {
        Assert.hasLength(input, "Input required");
        try
        {
            return input.getBytes(AppConstants.UTF8);
        }
        catch (UnsupportedEncodingException fallbackToDefault)
        {
            return input.getBytes();
        }
    }

    /**
     * Converts a byte array into a String using UTF-8, falling back to the
     * platform's default character set if UTF-8 fails.
     * 
     * @param byteArray
     *            the byte array to convert (required)
     * @return a string representation of the byte array
     */
    public static String byteArrayToString(byte[] byteArray)
    {
        Assert.notNull(byteArray, "ByteArray required");
        Assert.isTrue(byteArray.length > 0, "ByteArray cannot be empty");
        try
        {
            return new String(byteArray, AppConstants.UTF8);
        }
        catch (final UnsupportedEncodingException e)
        {
            return new String(byteArray);
        }
    }

    private static byte[] cipher(String key, byte[] passedBytes, int cipherMode)
    {
        try
        {
            final KeySpec keySpec = new DESedeKeySpec(stringToByteArray(key));
            final SecretKeyFactory keyFactory = SecretKeyFactory
                    .getInstance("DESede");
            final Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
            final SecretKey secretKey = keyFactory.generateSecret(keySpec);
            cipher.init(cipherMode, secretKey);
            return cipher.doFinal(passedBytes);
        }
        catch (InvalidKeyException | InvalidKeySpecException
                | NoSuchAlgorithmException | NoSuchPaddingException
                | BadPaddingException | IllegalBlockSizeException e)
        {
            throw new EncryptionException(e.getMessage(), e);
        }
    }

    /**
     * Encrypts the inputString using the key.
     * 
     * @param key
     *            at least 24 character long key (required)
     * @param inputString
     *            the string to encrypt (required)
     * @return the encrypted version of the inputString
     * @throws EncryptionException
     *             in the event of an encryption failure
     */
    public static String encrypt(String key, String inputString)
    {
        isValidKey(key);
        final byte[] cipherText = cipher(key, stringToByteArray(inputString),
                Cipher.ENCRYPT_MODE);
        return byteArrayToString(Base64.encode(cipherText));
    }

    /**
     * Encrypts the inputBytes using the key.
     * 
     * @param key
     *            at least 24 character long key (required)
     * @param inputBytes
     *            the bytes to encrypt (required)
     * @return the encrypted version of the inputBytes
     * @throws EncryptionException
     *             in the event of an encryption failure
     */
    public static byte[] encrypt(String key, byte[] inputBytes)
    {
        isValidKey(key);
        return Base64.encode(cipher(key, inputBytes, Cipher.ENCRYPT_MODE));
    }

    /**
     * Decrypts the inputString using the key.
     * 
     * @param key
     *            the key used to originally encrypt the string (required)
     * @param inputString
     *            the encrypted string (required)
     * @return the decrypted version of inputString
     * @throws EncryptionException
     *             in the event of an encryption failure
     */
    public static String decrypt(String key, String inputString)
    {
        Assert.hasText(key, "A key is required to attempt decryption");
        final byte[] cipherText = cipher(key,
                Base64.decode(stringToByteArray(inputString)),
                Cipher.DECRYPT_MODE);
        return byteArrayToString(cipherText);
    }

    /**
     * Decrypts the inputBytes using the key.
     * 
     * @param key
     *            the key used to originally encrypt the string (required)
     * @param inputBytes
     *            the encrypted bytes (required)
     * @return the decrypted version of inputBytes
     * @throws EncryptionException
     *             in the event of an encryption failure
     */
    public static byte[] decrypt(String key, byte[] inputBytes)
    {
        Assert.hasText(key, "A key is required to attempt decryption");
        return cipher(key, Base64.decode(inputBytes), Cipher.DECRYPT_MODE);
    }

    /**
     * checks whether the key is valid or not.<br>
     * Criteria for a valid key is
     * <ul>
     * <li>Must not be null/blank</li>
     * <li>must be of a minimum length 24 characters</li>
     * </ul>
     * 
     * @param key
     */
    private static void isValidKey(String key)
    {
        Assert.hasText(key, "A key to perform the encryption is required");
        Assert.isTrue(key.length() >= MINIMUM_KEY_LENGTH,
                "Key must be at least " + MINIMUM_KEY_LENGTH
                        + " characters long");
    }

    /**
     * Exception utility class for throwing any Encryption/Decryption related
     * exceptions.
     * 
     * @author Venkaiah Chowdary Koneru
     * @see NestedRuntimeException
     */
    public static class EncryptionException extends NestedRuntimeException
    {
        private static final long serialVersionUID = 1L;

        /**
         * Construct a <code>EncryptionException</code> with the specified
         * detail message and nested exception.
         * 
         * @param message
         *            the detail message
         * @param cause
         *            the nested exception
         */
        public EncryptionException(String message, Throwable cause)
        {
            super(message, cause);
        }

        /**
         * Construct a <code>EncryptionException</code> with the specified
         * detail message.
         * 
         * @param message
         *            the detail message
         */
        public EncryptionException(String message)
        {
            super(message);
        }
    }
}
